#!/usr/bin/env python
#Name: BrownieHelper
#Developed by: Sonora (The Dragonflight)
#Website: http://sonora.kirintor.googlepages.com/contents
#SVN: http://code.google.com/p/browniehelper/source/browse
#License: GNU General Public License v3

"""
Helper script that works in tandem with the BrownieHelper WoW addon.

Right now our lua 'parser' is extremely dumb.  It really only works on saved
variables files, which save text indexed lua tables with no other code. Requires
["str"] = x or "val", -- [n] notation.  Handles newlines in strings, and true
and false objects.  But that's it.

This is a barebones sort of a module -- very little polish or intelligent error
handling.

To get command line options run: python browniehelper.py --help
"""

__version__ = "0.9.3"

from sgmllib import SGMLParser
from optparse import OptionParser
from ConfigParser import ConfigParser
import htmlentitydefs, urllib, re, os, sys, datetime

#Useful for debugging
mod_name = "BrownieHelper"
db_name = "BrownieHelperDB2"

#Lua parsing & pp'ing
match_lists = re.compile(r'({\s*("(.*)", -- \[.*\]\s*){1,}})')
match_item = re.compile(r'"(?P<val>.*?)", -- \[.*?\]')

class luaParse(object):
    
    #Converts dicts/lists to lua tables    
    def convertValue(self, val):
        if type(val)==dict: return LuaTable(val)
        if type(val)==list: return LuaList(val)
        if type(val)==str: return val.replace("\n",r'\n')
        else: return val
        
    def __str__(self): return self.pp()

class LuaTable(luaParse, dict):
    def __init__(self, seed=None):
        """
        Seed should be a dict.
        """
        if seed:
            for k,v in seed.items(): self.addItem(k,v)
            
    #pp'er
    def pp(self, indentation=""):
        ret = "{\n"
        
        local_indent = indentation + "\t"
        
        for k,v in self.items():
            ret += local_indent + '["' + k + '"] = '
            
            if type(v)==LuaTable or type(v)==LuaList: ret+= v.pp(local_indent)
            elif type(v) == str: ret += '"' + v + '",\n'
            elif type(v) == list: ret += '"' + ", ".join(v) + '",\n'
            else: ret += str(v).replace("False",'false').replace("True",'true') + ",\n"
            
        if indentation!="": return ret + indentation + "},\n"
        else: return ret + indentation + "}\n"
    
    #Converts dicts to lua tables    
    def addItem(self, key, val): self[key]=self.convertValue(val)

class LuaList(luaParse, list): #models a Lua table with just values (printed to saved variables as "val", -- n)
    def __init__(self, seed=None):
        """
        Seed should be a dict.
        """
        if seed:
            for v in seed: self.addItem(v)
            
    def addItem(self, val): self.append(self.convertValue(val))
    
    #pp'er
    def pp(self, indentation=""):
        ret = "{\n"
        
        local_indent = indentation + "\t"
        
        for i,v in enumerate(self):
            ret += local_indent
            
            if type(v)==LuaTable or type(v)==LuaList: ret+= v.pp(local_indent)
            elif type(v) == str: ret += '"' + v + '"'
            elif type(v) == list: ret += '"' + ", ".join(v) + '",\n'
            else: ret += str(v).replace("False",'false').replace("True",'true')
            
            ret += ", -- [" + str(i+1) + "]\n"
            
        if indentation!="": return ret + indentation + "},\n"
        else: return ret + indentation + "}\n"


#HTML parsing
class TableParser(SGMLParser):
    def reset(self):

        #Main table reconstruction dictionary:  table_level : [rows]
        self.tables = {}

        #Row tracking list, [td element,]
        self.curr_row = None

        #Object that track the current table
        self.last_table_level = 0
        self.seed = 1
        self.table_level = []

        self.capture_text = False

        #Parental reset
        SGMLParser.reset(self)
        
    #Table element recognition        
    def start_table(self, attrs):
        if len(self.table_level) == self.last_table_level: self.seed = 1
        self.table_level.append(self.seed)
        self.last_table_level = len(self.table_level)
        self.tables[tuple(self.table_level)] = []
        
    def end_table(self):
        self.seed = self.table_level[-1] + 1
        self.table_level.pop(-1)
        
    #Row element recognition
    def start_tr(self, attrs):
        self.curr_row = []
        self.tables[tuple(self.table_level)].append(self.curr_row)
                
    def end_tr(self): self.curr_row = None
    #Column element recognition
    def start_td(self, attrs):
        if self.curr_row != None: self.capture_text=True
        
    def end_td(self): self.capture_text = False
    def start_th(self, attrs):
        if self.curr_row != None: self.capture_text = True

    def end_th(self): self.capture_text = False        
    #Plain data handling
    def handle_data(self, text):
        if self.capture_text==True: self.curr_row.append(text)


#Event log structure pp'ers
def plainTextReport(reportDb): #this is a model for how we would build an xml pp'er for CTRA_xml uploading the raids/events automatically to a eqDKP site (if we get the mod installed)
    "Generates a plain text report"
    #reportDb = {run_name : {event_title:event_info},
    
    #Calculate a sensible file name
    now = datetime.datetime.now()
    filename = 'BrownieHelperReport_' + str(now.month) + '-' + str(now.day) + "-" + str(now.year)[-2:] + '.txt'
    fs = open(os.path.join(os.getcwd(), filename),'w')
    
    for runName, runInfo in reportDb.items():
        fs.write("-- " + runName + " --\n\n")
        
        #Bin events by general type category
        eventTypes = [
            ("Reward Events", lambda eventInfo: eventInfo['event'] in reward_events),
            ("Auction Events", lambda eventInfo: eventInfo['event'] in auction_events),
            ("Point Events", lambda eventInfo: eventInfo['event'] in point_events),
            ("Other Events", lambda eventInfo: eventInfo['event'] not in reward_events and eventInfo['event'] not in auction_events and eventInfo['event'] not in point_events)
            ]
        eventMap = dict([(k,[]) for k,v in eventTypes])
        for eventTitle,eventInfo in runInfo.items():
            for eventType,checker in eventTypes:
                if checker(eventInfo): eventMap[eventType].append(eventInfo)
                
        #Pp the events by category
        for eventType,c in eventTypes:
            
            #If this category isn't empty
            if eventMap[eventType]:
                eventMap[eventType].sort(lambda eventInfoA,eventInfoB: cmp(eventInfoA['gametime'],eventInfoB['gametime']))
                fs.write("*"+eventType+"*\n\n")
                for eventInfo in eventMap[eventType]:
                    fs.write("Event: " + eventInfo['event'] + "\n")
                    fs.write("Time: " + eventInfo['gametime'] + "\n")
                    if eventInfo.has_key('player'): fs.write("Player: " + eventInfo['player'] + "\n")
                    if eventInfo.has_key('players'): fs.write("Players: " + eventInfo['players'] + "\n")
                    if eventInfo.has_key('itemname'): fs.write("Item: " + eventInfo['itemname'] + "\n")
                    if eventInfo.has_key('pointcost'): fs.write("Pointcost: " + eventInfo['pointcost'] + "\n")
                    if eventInfo.has_key('reward'): fs.write("Reward: " + eventInfo['reward'] + "\n")
                    for k,v in eventInfo.items():
                        if k not in ('event','gametime','uploaded','rewardevent','player','players','itemname','pointcost','reward'):
                            if type(v)==list: v = "\n" + "\n".join(v)
                            try: fs.write(k+": " + v + "\n")
                            except: print k, v
                    fs.write("\n")
                fs.write("\n")
        
    fs.close()
    print "Saved plain text report to: ", filename
    
#Queries for different times of events we can find in the runDbs
base_events = ["Raid start time", "Time adjustment"]
reward_events = base_events[:] #This list will get filled in when the guild configuration is loaded
point_events = ['Adjusted points',]
auction_events = ['Auction won',]

#Main bits of logic
def loadSavedVariables(saved_variables_path):
    """
    Adds in a layer of logic for handling unparsable or missing saved variables
    file.  Calls parseSavedVariables if available and garuntees a return of a
    valid BrownieHelperTable structure.
    """
    
    if os.path.exists(saved_variables_path):
        if not os.path.exists(os.path.join(saved_variables_path, mod_name + ".lua")):
            print "Creating new saved variables file:  " + mod_name + ".lua"
            fs = open(os.path.join(saved_variables_path, mod_name + ".lua"), 'w')
            fs.write(db_name + " = {}")
            fs.close()
        saved_variables_path = os.path.join(saved_variables_path, mod_name + ".lua")
        
    else:
        print "The supplied SavedVariablesPath is invalid."
        sys.exit()
    
    return parseSavedVariables(saved_variables_path)

def parseSavedVariables(saved_variables_path):
    
    #Open and 'parse' the file by exec'ing as python dicts
    fs = open(saved_variables_path)
    txt = fs.read()
    fs.close()
    
    txt = txt.replace('false','False').replace('true','True')
    
    for lst_match in match_lists.findall(txt):
        
        #for each list we find
        new_list = []
        for val in match_item.findall(lst_match[0]): new_list.append(val)
        txt = txt.replace(lst_match[0], "list(%<%" + repr(new_list) + "%>%)")
        
    txt = txt.replace("","").replace("\t","").replace("[",'').replace("]",'').replace(" = ",':').replace(db_name + ':', db_name + '=').replace("%<%","[").replace("%>%","]")
    
    try:
        exec txt
    except:
        print "Error parsing: "
        print txt
        raise SyntaxError("couldn't parse converted lua text")

    return LuaTable(locals()[db_name])

def updatePointsDb(realm_string, BrownieHelperTable, url_map):
    
    print "Updating local points for: ", realm_string
    
    for pool_name, url in url_map.items():
        print "Downloading & updating: ", pool_name
        
        #Retrieve & parse the page
        f = urllib.urlopen(url)
        page = f.read()
        p = TableParser()
        p.feed(page)
        data = p.tables[(2,)][2:-1]
        
        #Construct the lua table for points
        new_table = LuaTable()
        for line in data: new_table[line[0]] = line[-1] #Name is first, current points in the last column
        BrownieHelperTable['realms'][realm_string]['pointsDb'][pool_name] = new_table
        
    #Record a datetime stamp (in UTC) for this update
    t = datetime.datetime.utcnow()
    BrownieHelperTable['realms'][realm_string]["lastExternalUpdate"] = str(t.month) + "/" + str(t.day) + "/" + str(t.year)[-2:] + " " + str(t.hour) + ":" + str(t.minute)
    
def updateConfiguration(BrownieHelperTable, realm_string, tables):
    
    print "Updating configuration:"
    
    for name, table in tables.items():
        print name
        BrownieHelperTable['realms'][realm_string][name] = LuaTable(table)
    
def generateReports(realm_string, BrownieHelperTable, auto_check = True):
    print "Generating reports for events which have not been uploaded...", (auto_check and "(and marking them as uploaded)" or "")
    
    #Compile reportDb = {run_name : {event_title:event_info}, } for all unchecked events
    reportDb = {}
    for runName, runInfo in BrownieHelperTable['realms'][realm_string]['runDb'].items():
        
        events = {}
        for eventTitle, eventInfo in runInfo.get('events',{}).items():
            
            #If this is an unchecked event
            if not eventInfo['uploaded']:
                
                #Parse any value lists
                for k,v in eventInfo.items():
                    if type(v)==str and k!='description' and ", " in v: eventInfo[k] = v.split(", ")
                    
                #If we're autochecking -- check the event
                if auto_check: eventInfo['uploaded'] = True
                
                #Add this to the list of events
                events[eventTitle] = eventInfo
            
        if events: reportDb[runName] = events
        
    if reportDb:
        for report in reports: report(reportDb)

reports = [plainTextReport,]

if __name__ == '__main__':

    #Parse the local configuration file
    conf_parser = ConfigParser()
    conf_parser.optionxform = str #stop loosing case sensitivity
    print "Loading local config: ", "BrownieHelperLocal.cfg"
    conf_parser.read("BrownieHelperLocal.cfg")
    
    #If a local configuration file is provided
    if conf_parser.has_option("GuildConfiguration","file"):
        print "Loading guild config: ", conf_parser.get("GuildConfiguration","file")
        conf_parser.read(conf_parser.get("GuildConfiguration","file"))
        
    #If a guild configuration url is provided
    elif conf_parser.has_option("GuildConfiguration","url"):
        print "Loading guild config: ", conf_parser.get("GuildConfiguration","url")
        conf_parser.readfp(urllib.urlopen(conf_parser.get("GuildConfiguration","url")))
        
    #Lets hope the guild configuration options are present in the local .cfg file
    else: print "No explicit guild configuration found.  This may end badly."
    
    #If no Wow SavedVariables path is provided, attempt to guess at one
    if conf_parser.has_option("SavedVariablesPath","path"): default_path = conf_parser.get("SavedVariablesPath","path")
    else:
        print "Guessing saved variables path... (may fail if you have more than one account)"
        
        currDir = os.getcwd()
        #We need to back track /interface/addons/browniehelper/script
        base_account_path = os.path.join(os.path.split(os.path.split(os.path.split(os.path.split(os.getcwd())[0])[0])[0])[0], "WTF", "Account")
        
        default_path=""
        if os.path.exists(base_account_path):
            
            dirs = [d for d in os.listdir(base_account_path) if os.path.isdir(os.path.join(base_account_path, d))]
            if len(dirs) and os.path.exists(os.path.join(base_account_path, dirs[0], "SavedVariables")): default_path = os.path.join(base_account_path, dirs[0], "SavedVariables")
    
    #Load the url map from the configuration info: url_map = {instance:url,}
    url_map = dict([(opt, conf_parser.get("PointSites",opt)) for opt in conf_parser.options("PointSites")])
    
    #Read in settings maps from the configuration file
    tables = {}
    for section in ["MinimumBids","LootTypes","Permissions","BossEvents","EventRewards","DefaultBiddingSequence"]:
        new_map = tables[section] = {}
        for option in conf_parser.options(section):
            value = conf_parser.get(section,option)
            if "," in value:
                value = value.replace(", ",",").split(",")
                if value[-1]=="": del value[-1]
            if value!='': new_map[option] = value
    
    #Use the boss events to update the reward events list
    for bosses in tables['BossEvents'].values():
        for boss in bosses: reward_events.append(boss)
        
    #Create a new entry in tables that appends the built in events to boss events
    Events = tables["Events"] = {}
    for name,lst in tables["BossEvents"].items():
        Events[name] = base_events + lst
    
    #setup the command line options
    opt_parser = OptionParser(version="%prog " + __version__)
    opt_parser.add_option("-p", "--path", action="store", type="string", dest="path", metavar="PATH", default=default_path, help="Directory path to your account-specific SavedVariables directory [default: %default]")
    
    opt_parser.add_option("-r", "--realm", action="store", type="string", dest="realm", default=conf_parser.get("Server","realm"), metavar="REALM_NAME", help = "The name of the realm profile which the script should work with [default: %default]")
    opt_parser.add_option("-f", "--faction", action="store", type="string", dest="faction", default=conf_parser.get("Server","faction"), metavar="FACTION", help="The name of the faction profile which the script should work with [default: %default]")
    
    opt_parser.add_option("-u", "--no_update", action="store_false", dest="update", default=True, help="Skips updating the local saved variables file by downloading point totals from the eqDKP website")
    opt_parser.add_option("-m", "--no_report", action="store_false", dest="makereport", default=True, help="Skips generating reports for all un-uploaded events found in the saved variables file")
    opt_parser.add_option("-c", "--no_check", action="store_false", dest="auto_check", default=True, help="Skips automatically checking all reported events")
    
    #get the options we were passed
    (options, args) = opt_parser.parse_args()
    
    #Munge the realm string
    realm_string = options.realm + " - " + options.faction
    
    #Parse the saved variables file and make sure the mapping is complete-ish
    BrownieHelperTable=loadSavedVariables(options.path)
    if not BrownieHelperTable.has_key('realms'): BrownieHelperTable['realms'] = LuaTable()
    if not BrownieHelperTable['realms'].has_key(realm_string): BrownieHelperTable['realms'][realm_string] = LuaTable()
    if not BrownieHelperTable['realms'][realm_string].has_key('pointsDb'): BrownieHelperTable['realms'][realm_string]['pointsDb'] = LuaTable()
    if not BrownieHelperTable['realms'][realm_string].has_key('runDb'): BrownieHelperTable['realms'][realm_string]['runDb'] = LuaTable()
    updateConfiguration(BrownieHelperTable=BrownieHelperTable, realm_string=realm_string, tables=tables)
    
    #Do the work we've been asked to
    if options.update: updatePointsDb(realm_string=realm_string, BrownieHelperTable=BrownieHelperTable, url_map=url_map)
    if options.makereport: generateReports(realm_string=realm_string, BrownieHelperTable=BrownieHelperTable, auto_check=options.auto_check)
    
    print "Rewriting saved variables file..."
    fs = open(os.path.join(options.path, mod_name + ".lua") , 'w')
    fs.write(db_name + " = " + str(BrownieHelperTable))
    fs.close()
    
    #And we're done.
    print "Done."

